--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 54ccc03c4da62c3406f3871c9db7fd8375374998
Parents : bc40dcf
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-04T12:42:02-06:00
feat(tests): add unit tests for BanishedPage and RNPathPage components, enhancing coverage for blocked items and path management
Changes
5 files changed, 280 insertions(+), 8 deletions(-)
Diff
diff --git a/tests/frontend/BanishedPage.test.js b/tests/frontend/BanishedPage.test.js
new file mode 100644
index 00000000..f83972df
--- /dev/null
+++ b/tests/frontend/BanishedPage.test.js
@@ -0,0 +1,119 @@
+import { mount } from "@vue/test-utils";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import BlockedPage from "@/components/blocked/BlockedPage.vue";
+import GlobalState from "@/js/GlobalState";
+
+describe("BlockedPage.vue (Banished UI)", () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = {
+ get: vi.fn(),
+ post: vi.fn(),
+ delete: vi.fn(),
+ };
+ window.axios = axiosMock;
+
+ // Mock localization
+ const t = (key) => {
+ const translations = {
+ "common.save": "Save",
+ "common.cancel": "Cancel",
+ };
+ return translations[key] || key;
+ };
+
+ axiosMock.get.mockImplementation((url) => {
+ if (url === "/api/v1/blocked-destinations") {
+ return Promise.resolve({
+ data: {
+ blocked_destinations: [
+ { destination_hash: "a".repeat(32), created_at: "2026-01-04T12:00:00Z" },
+ ],
+ },
+ });
+ }
+ if (url === "/api/v1/reticulum/blackhole") {
+ return Promise.resolve({
+ data: {
+ blackholed_identities: {
+ ["b".repeat(32)]: {
+ source: "c".repeat(32),
+ reason: "Spam",
+ until: null,
+ },
+ },
+ },
+ });
+ }
+ if (url === "/api/v1/announces") {
+ return Promise.resolve({ data: { announces: [] } });
+ }
+ return Promise.resolve({ data: {} });
+ });
+ });
+
+ afterEach(() => {
+ delete window.axios;
+ });
+
+ const mountBlockedPage = () => {
+ return mount(BlockedPage, {
+ global: {
+ mocks: {
+ $t: (key) => key,
+ },
+ stubs: {
+ MaterialDesignIcon: {
+ template: '<div class="mdi-stub" :data-icon-name="iconName"></div>',
+ props: ["iconName"],
+ },
+ },
+ },
+ });
+ };
+
+ it("displays 'Banished' title and subtext", async () => {
+ const wrapper = mountBlockedPage();
+ // Wait for isLoading to become false
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ expect(wrapper.text()).toContain("Banished");
+ expect(wrapper.text()).toContain("Manage Banished users and nodes");
+ });
+
+ it("combines local blocked and RNS blackholed items", async () => {
+ const wrapper = mountBlockedPage();
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ expect(wrapper.vm.allBlockedItems.length).toBe(2);
+
+ const rnsItem = wrapper.vm.allBlockedItems.find((i) => i.is_rns_blackholed);
+ expect(rnsItem).toBeDefined();
+ expect(rnsItem.destination_hash).toBe("b".repeat(32));
+ expect(rnsItem.rns_reason).toBe("Spam");
+ });
+
+ it("displays RNS Blackhole badge for blackholed items", async () => {
+ const wrapper = mountBlockedPage();
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ expect(wrapper.text()).toContain("RNS Blackhole");
+ });
+
+ it("calls delete API when lifting banishment", async () => {
+ // Mock DialogUtils.confirm
+ const DialogUtils = await import("@/js/DialogUtils");
+ vi.spyOn(DialogUtils.default, "confirm").mockResolvedValue(true);
+
+ const wrapper = mountBlockedPage();
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ const unblockButtons = wrapper.findAll("button").filter((b) => b.text().includes("Lift Banishment"));
+ expect(unblockButtons.length).toBeGreaterThan(0);
+
+ await unblockButtons[0].trigger("click");
+
+ expect(axiosMock.delete).toHaveBeenCalled();
+ });
+});
diff --git a/tests/frontend/LanguageSelector.test.js b/tests/frontend/LanguageSelector.test.js
index fff58977..8e77e9e0 100644
--- a/tests/frontend/LanguageSelector.test.js
+++ b/tests/frontend/LanguageSelector.test.js
@@ -14,6 +14,7 @@ describe("LanguageSelector.vue", () => {
},
stubs: {
MaterialDesignIcon: true,
+ Teleport: true,
},
},
});
@@ -28,20 +29,20 @@ describe("LanguageSelector.vue", () => {
const wrapper = mountLanguageSelector();
const button = wrapper.find("button");
- expect(wrapper.find(".absolute").exists()).toBe(false);
+ expect(wrapper.find(".fixed").exists()).toBe(false);
await button.trigger("click");
- expect(wrapper.find(".absolute").exists()).toBe(true);
+ expect(wrapper.find(".fixed").exists()).toBe(true);
await button.trigger("click");
- expect(wrapper.find(".absolute").exists()).toBe(false);
+ expect(wrapper.find(".fixed").exists()).toBe(false);
});
it("lists all available languages in the dropdown", async () => {
const wrapper = mountLanguageSelector();
await wrapper.find("button").trigger("click");
- const languageButtons = wrapper.findAll(".absolute button");
+ const languageButtons = wrapper.findAll(".fixed button");
expect(languageButtons).toHaveLength(3);
expect(languageButtons[0].text()).toContain("English");
expect(languageButtons[1].text()).toContain("Deutsch");
@@ -52,22 +53,22 @@ describe("LanguageSelector.vue", () => {
const wrapper = mountLanguageSelector("en");
await wrapper.find("button").trigger("click");
- const deButton = wrapper.findAll(".absolute button")[1];
+ const deButton = wrapper.findAll(".fixed button")[1];
await deButton.trigger("click");
expect(wrapper.emitted("language-change")).toBeTruthy();
expect(wrapper.emitted("language-change")[0]).toEqual(["de"]);
- expect(wrapper.find(".absolute").exists()).toBe(false);
+ expect(wrapper.find(".fixed").exists()).toBe(false);
});
it("does not emit language-change when the current language is selected", async () => {
const wrapper = mountLanguageSelector("en");
await wrapper.find("button").trigger("click");
- const enButton = wrapper.findAll(".absolute button")[0];
+ const enButton = wrapper.findAll(".fixed button")[0];
await enButton.trigger("click");
expect(wrapper.emitted("language-change")).toBeFalsy();
- expect(wrapper.find(".absolute").exists()).toBe(false);
+ expect(wrapper.find(".fixed").exists()).toBe(false);
});
});
diff --git a/tests/frontend/NotificationBell.test.js b/tests/frontend/NotificationBell.test.js
index 743291c1..c1925d8f 100644
--- a/tests/frontend/NotificationBell.test.js
+++ b/tests/frontend/NotificationBell.test.js
@@ -32,6 +32,7 @@ describe("NotificationBell.vue", () => {
},
stubs: {
MaterialDesignIcon: true,
+ Teleport: true,
},
directives: {
"click-outside": {},
@@ -172,6 +173,7 @@ describe("NotificationBell.vue", () => {
},
stubs: {
MaterialDesignIcon: true,
+ Teleport: true,
},
directives: {
"click-outside": {},
diff --git a/tests/frontend/RNPathPage.test.js b/tests/frontend/RNPathPage.test.js
new file mode 100644
index 00000000..7fcf42c2
--- /dev/null
+++ b/tests/frontend/RNPathPage.test.js
@@ -0,0 +1,105 @@
+import { mount } from "@vue/test-utils";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import RNPathPage from "@/components/tools/RNPathPage.vue";
+
+describe("RNPathPage.vue", () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = {
+ get: vi.fn(),
+ post: vi.fn(),
+ };
+ window.axios = axiosMock;
+
+ axiosMock.get.mockImplementation((url) => {
+ if (url === "/api/v1/rnpath/table") {
+ return Promise.resolve({
+ data: {
+ table: [
+ {
+ hash: "a".repeat(32),
+ hops: 1,
+ via: "b".repeat(32),
+ interface: "UDP",
+ expires: 1234567890,
+ },
+ ],
+ },
+ });
+ }
+ if (url === "/api/v1/rnpath/rates") {
+ return Promise.resolve({
+ data: {
+ rates: [
+ {
+ hash: "c".repeat(32),
+ last: 1234567890,
+ timestamps: [],
+ rate_violations: 0,
+ blocked_until: 0,
+ },
+ ],
+ },
+ });
+ }
+ return Promise.resolve({ data: {} });
+ });
+ });
+
+ afterEach(() => {
+ delete window.axios;
+ });
+
+ const mountRNPathPage = () => {
+ return mount(RNPathPage, {
+ global: {
+ mocks: {
+ $t: (key) => key,
+ },
+ stubs: {
+ MaterialDesignIcon: {
+ template: '<div class="mdi-stub" :data-icon-name="iconName"></div>',
+ props: ["iconName"],
+ },
+ },
+ },
+ });
+ };
+
+ it("renders and loads data", async () => {
+ const wrapper = mountRNPathPage();
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ expect(wrapper.text()).toContain("RNPath");
+ expect(wrapper.vm.pathTable.length).toBe(1);
+ expect(wrapper.vm.rateTable.length).toBe(1);
+ });
+
+ it("switches tabs", async () => {
+ const wrapper = mountRNPathPage();
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ const ratesButton = wrapper.findAll("button").find((b) => b.text() === "Rates");
+ await ratesButton.trigger("click");
+ expect(wrapper.vm.tab).toBe("rates");
+
+ const actionsButton = wrapper.findAll("button").find((b) => b.text() === "Actions");
+ await actionsButton.trigger("click");
+ expect(wrapper.vm.tab).toBe("actions");
+ });
+
+ it("calls request path API", async () => {
+ const wrapper = mountRNPathPage();
+ await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false));
+
+ await wrapper.setData({ tab: "actions", requestHash: "d".repeat(32) });
+
+ const requestButton = wrapper.findAll("button").find((b) => b.text() === "Request");
+ await requestButton.trigger("click");
+
+ expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/rnpath/request", {
+ destination_hash: "d".repeat(32),
+ });
+ });
+});
diff --git a/tests/frontend/UIThemeAndVisibility.test.js b/tests/frontend/UIThemeAndVisibility.test.js
index c33e6038..13e65657 100644
--- a/tests/frontend/UIThemeAndVisibility.test.js
+++ b/tests/frontend/UIThemeAndVisibility.test.js
@@ -8,6 +8,14 @@ import ChangelogModal from "../../meshchatx/src/frontend/components/ChangelogMod
import NotificationBell from "../../meshchatx/src/frontend/components/NotificationBell.vue";
import LanguageSelector from "../../meshchatx/src/frontend/components/LanguageSelector.vue";
+vi.mock("vuetify", () => ({
+ useTheme: vi.fn(() => ({
+ global: {
+ name: { value: "light" },
+ },
+ })),
+}));
+
vi.mock("../../meshchatx/src/frontend/js/WebSocketConnection", () => ({
default: {
on: vi.fn(),
@@ -344,6 +352,7 @@ describe("Visibility Checks", () => {
banished_effect_enabled: true,
banished_text: "BANISHED",
banished_color: "#dc2626",
+ blackhole_integration_enabled: true,
},
},
}),
@@ -379,6 +388,42 @@ describe("Visibility Checks", () => {
delete window.axios;
});
+ it("SettingsPage shows blackhole integration toggle", async () => {
+ const axiosMock = {
+ get: vi.fn().mockResolvedValue({
+ data: {
+ config: {
+ blackhole_integration_enabled: true,
+ },
+ },
+ }),
+ patch: vi.fn().mockResolvedValue({ data: {} }),
+ };
+ window.axios = axiosMock;
+
+ const wrapper = mount(SettingsPage, {
+ global: {
+ stubs: {
+ MaterialDesignIcon: { template: "<div></div>" },
+ Toggle: Toggle,
+ ShortcutRecorder: { template: "<div></div>" },
+ RouterLink: { template: "<a><slot /></a>" },
+ },
+ mocks: {
+ $t: (key) => key,
+ $router: { push: vi.fn() },
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.text()).toContain("app.blackhole_integration_enabled");
+
+ delete window.axios;
+ });
+
it("SettingsPage hides banished config when toggle is disabled", async () => {
const axiosMock = {
get: vi.fn().mockResolvedValue({
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────